// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package commands

import (
	"context"
	"fmt"

	"github.com/mattermost/mattermost/server/public/model"

	"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
	"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"

	"github.com/hashicorp/go-multierror"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
)

var BotCmd = &cobra.Command{
	Use:   "bot",
	Short: "Management of bots",
}

var CreateBotCmd = &cobra.Command{
	Use:     "create [username]",
	Short:   "Create bot",
	Long:    "Create bot.",
	Example: `  bot create testbot`,
	PreRun:  disableLocalPrecheck,
	RunE:    withClient(botCreateCmdF),
	Args:    cobra.ExactArgs(1),
}

var UpdateBotCmd = &cobra.Command{
	Use:     "update [username]",
	Short:   "Update bot",
	Long:    "Update bot information.",
	Example: `  bot update testbot --username newbotusername`,
	RunE:    withClient(botUpdateCmdF),
	Args:    cobra.ExactArgs(1),
}

var ListBotCmd = &cobra.Command{
	Use:     "list",
	Short:   "List bots",
	Long:    "List the bots users.",
	Example: `  bot list`,
	RunE:    withClient(botListCmdF),
	Args:    cobra.NoArgs,
}

var DisableBotCmd = &cobra.Command{
	Use:     "disable [username]",
	Short:   "Disable bot",
	Long:    "Disable an enabled bot",
	Example: `  bot disable testbot`,
	RunE:    withClient(botDisableCmdF),
	Args:    cobra.MinimumNArgs(1),
}

var EnableBotCmd = &cobra.Command{
	Use:     "enable [username]",
	Short:   "Enable bot",
	Long:    "Enable a disabled bot",
	Example: `  bot enable testbot`,
	RunE:    withClient(botEnableCmdF),
	Args:    cobra.MinimumNArgs(1),
}

var AssignBotCmd = &cobra.Command{
	Use:     "assign [bot-username] [new-owner-username]",
	Short:   "Assign bot",
	Long:    "Assign the ownership of a bot to another user",
	Example: `  bot assign testbot user2`,
	RunE:    withClient(botAssignCmdF),
	Args:    cobra.ExactArgs(2),
}

func init() {
	CreateBotCmd.Flags().String("display-name", "", "Optional. The display name for the new bot.")
	CreateBotCmd.Flags().String("description", "", "Optional. The description text for the new bot.")
	CreateBotCmd.Flags().Bool("with-token", false, "Optional. Auto genreate access token for the bot.")
	ListBotCmd.Flags().Bool("orphaned", false, "Optional. Only show orphaned bots.")
	ListBotCmd.Flags().Bool("all", false, "Optional. Show all bots (including deleleted and orphaned).")
	UpdateBotCmd.Flags().String("username", "", "Optional. The new username for the bot.")
	UpdateBotCmd.Flags().String("display-name", "", "Optional. The new display name for the bot.")
	UpdateBotCmd.Flags().String("description", "", "Optional. The new description text for the bot.")

	BotCmd.AddCommand(
		CreateBotCmd,
		UpdateBotCmd,
		ListBotCmd,
		EnableBotCmd,
		DisableBotCmd,
		AssignBotCmd,
	)

	RootCmd.AddCommand(BotCmd)
}

func botCreateCmdF(c client.Client, cmd *cobra.Command, args []string) error {
	username := args[0]
	displayName, _ := cmd.Flags().GetString("display-name")
	description, _ := cmd.Flags().GetString("description")

	bot, _, err := c.CreateBot(context.TODO(), &model.Bot{
		Username:    username,
		DisplayName: displayName,
		Description: description,
	})
	if err != nil {
		return errors.Errorf("could not create bot: %s", err)
	}

	printer.PrintT("Created bot {{.UserId}}", bot)

	if withToken, _ := cmd.Flags().GetBool("with-token"); withToken {
		return generateTokenForAUserCmdF(c, cmd, []string{args[0], "autogenerated"})
	}

	return nil
}

func botUpdateCmdF(c client.Client, cmd *cobra.Command, args []string) error {
	if !cmd.Flags().Changed("username") && !cmd.Flags().Changed("display-name") && !cmd.Flags().Changed("description") {
		return errors.New("at least one of --username, --display-name or --description must be set")
	}

	user := getUserFromUserArg(c, args[0])
	if user == nil {
		return errors.New("unable to find user '" + args[0] + "'")
	}
	patch := model.BotPatch{}
	username, err := cmd.Flags().GetString("username")
	if err == nil && cmd.Flags().Changed("username") {
		patch.Username = &username
	}
	displayName, err := cmd.Flags().GetString("display-name")
	if err == nil && cmd.Flags().Changed("display-name") {
		patch.DisplayName = &displayName
	}
	description, err := cmd.Flags().GetString("description")
	if err == nil && cmd.Flags().Changed("description") {
		patch.Description = &description
	}

	bot, _, err := c.PatchBot(context.TODO(), user.Id, &patch)
	if err != nil {
		return errors.Errorf("could not update bot: %s", err)
	}

	printer.PrintT("Updated bot {{.UserId}} ({{.Username}})", bot)

	return nil
}

func botListCmdF(c client.Client, cmd *cobra.Command, args []string) error {
	orphaned, _ := cmd.Flags().GetBool("orphaned")
	all, _ := cmd.Flags().GetBool("all")

	page := 0
	perPage := DefaultPageSize
	tpl := `{{.UserId}}: {{.Username}}`
	for {
		var bots []*model.Bot
		var err error
		if all { //nolint:gocritic
			bots, _, err = c.GetBotsIncludeDeleted(context.TODO(), page, perPage, "")
		} else if orphaned {
			bots, _, err = c.GetBotsOrphaned(context.TODO(), page, perPage, "")
		} else {
			bots, _, err = c.GetBots(context.TODO(), page, perPage, "")
		}
		if err != nil {
			return errors.Wrap(err, "Failed to fetch bots")
		}

		userIds := []string{}
		for _, bot := range bots {
			userIds = append(userIds, bot.OwnerId)
		}

		users, _, err := c.GetUsersByIds(context.TODO(), userIds)
		if err != nil {
			return errors.Wrap(err, "Failed to fetch bots")
		}

		usersByID := map[string]*model.User{}
		for _, user := range users {
			usersByID[user.Id] = user
		}

		var ownerName string
		var ownerDeleteAt int64
		for _, bot := range bots {
			if owner, ok := usersByID[bot.OwnerId]; ok {
				ownerName = owner.Username
				ownerDeleteAt = owner.DeleteAt
			} else {
				// not all bots have a userId in their ownerId field.
				ownerName = bot.OwnerId
				ownerDeleteAt = 0
			}
			tplExtraText := fmt.Sprintf("(Owned by %s, {{if ne .DeleteAt 0}}Disabled{{else}}Enabled{{end}}{{if ne %d 0}}, Orphaned{{end}})", ownerName, ownerDeleteAt)
			printer.PrintT(tpl+tplExtraText, bot)
		}

		if len(bots) < perPage {
			break
		}

		page++
	}

	return nil
}

func botEnableCmdF(c client.Client, cmd *cobra.Command, args []string) error {
	users := getUsersFromUserArgs(c, args)

	var result *multierror.Error

	for i, user := range users {
		if user == nil {
			printer.PrintError(fmt.Sprintf("can't find user '%v'", args[i]))
			result = multierror.Append(result, fmt.Errorf("can't find user %q", args[i]))
			continue
		}

		bot, _, err := c.EnableBot(context.TODO(), user.Id)
		if err != nil {
			printer.PrintError(fmt.Sprintf("could not enable bot '%v'", args[i]))
			result = multierror.Append(result, fmt.Errorf("could not enable bot %q: %w", args[i], err))
			continue
		}

		printer.PrintT("Enabled bot {{.UserId}} ({{.Username}})", bot)
	}

	return result.ErrorOrNil()
}

func botDisableCmdF(c client.Client, cmd *cobra.Command, args []string) error {
	users := getUsersFromUserArgs(c, args)

	var result *multierror.Error
	for i, user := range users {
		if user == nil {
			printer.PrintError(fmt.Sprintf("can't find user '%v'", args[i]))
			result = multierror.Append(result, fmt.Errorf("can't find user %q", args[i]))
			continue
		}

		bot, _, err := c.DisableBot(context.TODO(), user.Id)
		if err != nil {
			printer.PrintError(fmt.Sprintf("could not disable bot '%v'", args[i]))
			result = multierror.Append(result, fmt.Errorf("could not disable bot %q: %w", args[i], err))
			continue
		}

		printer.PrintT("Disabled bot {{.UserId}} ({{.Username}})", bot)
	}

	return result.ErrorOrNil()
}

func botAssignCmdF(c client.Client, cmd *cobra.Command, args []string) error {
	botUser := getUserFromUserArg(c, args[0])
	if botUser == nil {
		return errors.New("unable to find user '" + args[0] + "'")
	}
	newOwnerUser := getUserFromUserArg(c, args[1])
	if newOwnerUser == nil {
		return errors.New("unable to find user '" + args[1] + "'")
	}

	newBot, _, err := c.AssignBot(context.TODO(), botUser.Id, newOwnerUser.Id)
	if err != nil {
		return errors.Errorf("can not assign bot '%s' to user '%s'", args[0], args[1])
	}

	printer.PrintT("The bot {{.UserId}} ({{.Username}}) now belongs to the user "+newOwnerUser.Username, newBot)
	return nil
}
